[[...path]].page.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922
  1. import type React from 'react';
  2. import type { JSX, ReactNode } from 'react';
  3. import { useEffect } from 'react';
  4. import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
  5. import dynamic from 'next/dynamic';
  6. import Head from 'next/head';
  7. import { useRouter } from 'next/router';
  8. import type {
  9. IDataWithMeta,
  10. IPageInfo,
  11. IPagePopulatedToShowRevision,
  12. } from '@growi/core';
  13. import { isIPageInfo } from '@growi/core';
  14. import { isClient, pagePathUtils, pathUtils } from '@growi/core/dist/utils';
  15. import ExtensibleCustomError from 'extensible-custom-error';
  16. import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
  17. import superjson from 'superjson';
  18. import { BasicLayout } from '~/components/Layout/BasicLayout';
  19. import { PageView } from '~/components/PageView/PageView';
  20. import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
  21. import {
  22. SupportedAction,
  23. type SupportedActionType,
  24. } from '~/interfaces/activity';
  25. import type { CrowiRequest } from '~/interfaces/crowi-request';
  26. import { RegistrationMode } from '~/interfaces/registration-mode';
  27. import type { RendererConfig } from '~/interfaces/services/renderer';
  28. import type { ISidebarConfig } from '~/interfaces/sidebar-config';
  29. import type { CurrentPageYjsData } from '~/interfaces/yjs';
  30. import type { PageDocument, PageModel } from '~/server/models/page';
  31. import type { PageRedirectModel } from '~/server/models/page-redirect';
  32. import { useEditorModeClassName } from '~/services/layout/use-editor-mode-class-name';
  33. import { useEditingMarkdown } from '~/stores/editor';
  34. import {
  35. useCurrentPageId,
  36. useIsLatestRevision,
  37. useIsNotFound,
  38. useSWRMUTxCurrentPage,
  39. useSWRxCurrentPage,
  40. useTemplateBodyData,
  41. useTemplateTagData,
  42. } from '~/stores/page';
  43. import { useRedirectFrom } from '~/stores/page-redirect';
  44. import { useRemoteRevisionId } from '~/stores/remote-latest-page';
  45. import {
  46. useSetupGlobalSocket,
  47. useSetupGlobalSocketForPage,
  48. } from '~/stores/websocket';
  49. import {
  50. useCurrentPageYjsData,
  51. useSWRMUTxCurrentPageYjsData,
  52. } from '~/stores/yjs';
  53. import {
  54. useCurrentPathname,
  55. useCurrentUser,
  56. useDefaultIndentSize,
  57. useDisableLinkSharing,
  58. useElasticsearchMaxBodyLengthToIndex,
  59. useGrowiCloudUri,
  60. useIsAclEnabled,
  61. useIsAiEnabled,
  62. useIsAllReplyShown,
  63. useIsBulkExportPagesEnabled,
  64. useIsContainerFluid,
  65. useIsEnabledAttachTitleHeader,
  66. useIsEnabledMarp,
  67. useIsEnabledStaleNotification,
  68. useIsForbidden,
  69. useIsGuestUser,
  70. useIsIdenticalPath,
  71. useIsIndentSizeForced,
  72. useIsLocalAccountRegistrationEnabled,
  73. useIsNotCreatable,
  74. useIsPdfBulkExportEnabled,
  75. useIsRomUserAllowedToComment,
  76. useIsSearchPage,
  77. useIsSearchScopeChildrenAsDefault,
  78. useIsSearchServiceConfigured,
  79. useIsSearchServiceReachable,
  80. useIsSharedUser,
  81. useIsSlackConfigured,
  82. useIsUploadAllFileAllowed,
  83. useIsUploadEnabled,
  84. useIsUsersHomepageDeletionEnabled,
  85. useLimitLearnablePageCountPerAssistant,
  86. useRendererConfig,
  87. useShowPageSideAuthors,
  88. } from '~/stores-universal/context';
  89. import loggerFactory from '~/utils/logger';
  90. import type { NextPageWithLayout } from './_app.page';
  91. import type { CommonProps } from './utils/commons';
  92. import {
  93. addActivity,
  94. generateCustomTitleForPage,
  95. getNextI18NextConfig,
  96. getServerSideCommonProps,
  97. skipSSR,
  98. useInitSidebarConfig,
  99. } from './utils/commons';
  100. const GrowiContextualSubNavigationSubstance = dynamic(
  101. () => import('~/client/components/Navbar/GrowiContextualSubNavigation'),
  102. { ssr: false },
  103. );
  104. const GrowiPluginsActivator = dynamic(
  105. () =>
  106. import('~/features/growi-plugin/client/components').then(
  107. (mod) => mod.GrowiPluginsActivator,
  108. ),
  109. { ssr: false },
  110. );
  111. const DisplaySwitcher = dynamic(
  112. () =>
  113. import('~/client/components/Page/DisplaySwitcher').then(
  114. (mod) => mod.DisplaySwitcher,
  115. ),
  116. { ssr: false },
  117. );
  118. const PageStatusAlert = dynamic(
  119. () =>
  120. import('~/client/components/PageStatusAlert').then(
  121. (mod) => mod.PageStatusAlert,
  122. ),
  123. { ssr: false },
  124. );
  125. const UnsavedAlertDialog = dynamic(
  126. () => import('~/client/components/UnsavedAlertDialog'),
  127. { ssr: false },
  128. );
  129. const DescendantsPageListModal = dynamic(
  130. () =>
  131. import('~/client/components/DescendantsPageListModal').then(
  132. (mod) => mod.DescendantsPageListModal,
  133. ),
  134. { ssr: false },
  135. );
  136. const DrawioModal = dynamic(
  137. () =>
  138. import('~/client/components/PageEditor/DrawioModal').then(
  139. (mod) => mod.DrawioModal,
  140. ),
  141. { ssr: false },
  142. );
  143. const HandsontableModal = dynamic(
  144. () =>
  145. import('~/client/components/PageEditor/HandsontableModal').then(
  146. (mod) => mod.HandsontableModal,
  147. ),
  148. { ssr: false },
  149. );
  150. const TemplateModal = dynamic(
  151. () =>
  152. import('~/client/components/TemplateModal').then(
  153. (mod) => mod.TemplateModal,
  154. ),
  155. { ssr: false },
  156. );
  157. const LinkEditModal = dynamic(
  158. () =>
  159. import('~/client/components/PageEditor/LinkEditModal').then(
  160. (mod) => mod.LinkEditModal,
  161. ),
  162. { ssr: false },
  163. );
  164. const TagEditModal = dynamic(
  165. () =>
  166. import('~/client/components/PageTags/TagEditModal').then(
  167. (mod) => mod.TagEditModal,
  168. ),
  169. { ssr: false },
  170. );
  171. const ConflictDiffModal = dynamic(
  172. () =>
  173. import('~/client/components/PageEditor/ConflictDiffModal').then(
  174. (mod) => mod.ConflictDiffModal,
  175. ),
  176. { ssr: false },
  177. );
  178. const EditablePageEffects = dynamic(
  179. () =>
  180. import('~/client/components/Page/EditablePageEffects').then(
  181. (mod) => mod.EditablePageEffects,
  182. ),
  183. { ssr: false },
  184. );
  185. const logger = loggerFactory('growi:pages:all');
  186. const { isPermalink: _isPermalink, isCreatablePage } = pagePathUtils;
  187. const { removeHeadingSlash } = pathUtils;
  188. type IPageToShowRevisionWithMeta = IDataWithMeta<
  189. IPagePopulatedToShowRevision & PageDocument,
  190. IPageInfo
  191. >;
  192. type IPageToShowRevisionWithMetaSerialized = IDataWithMeta<string, string>;
  193. superjson.registerCustom<
  194. IPageToShowRevisionWithMeta,
  195. IPageToShowRevisionWithMetaSerialized
  196. >(
  197. {
  198. isApplicable: (v): v is IPageToShowRevisionWithMeta => {
  199. return v?.data != null && v?.data.toObject != null && isIPageInfo(v.meta);
  200. },
  201. serialize: (v) => {
  202. return {
  203. data: superjson.stringify(v.data.toObject()),
  204. meta: superjson.stringify(v.meta),
  205. };
  206. },
  207. deserialize: (v) => {
  208. return {
  209. data: superjson.parse(v.data),
  210. meta: v.meta != null ? superjson.parse(v.meta) : undefined,
  211. };
  212. },
  213. },
  214. 'IPageToShowRevisionWithMetaTransformer',
  215. );
  216. // GrowiContextualSubNavigation for NOT shared page
  217. type GrowiContextualSubNavigationProps = {
  218. isLinkSharingDisabled: boolean;
  219. };
  220. const GrowiContextualSubNavigation = (
  221. props: GrowiContextualSubNavigationProps,
  222. ): JSX.Element => {
  223. const { isLinkSharingDisabled } = props;
  224. const { data: currentPage } = useSWRxCurrentPage();
  225. return (
  226. <GrowiContextualSubNavigationSubstance
  227. currentPage={currentPage}
  228. isLinkSharingDisabled={isLinkSharingDisabled}
  229. />
  230. );
  231. };
  232. type Props = CommonProps & {
  233. pageWithMeta: IPageToShowRevisionWithMeta | null;
  234. // pageUser?: any,
  235. redirectFrom?: string;
  236. // shareLinkId?: string;
  237. isLatestRevision?: boolean;
  238. isIdenticalPathPage?: boolean;
  239. isForbidden: boolean;
  240. isNotFound: boolean;
  241. isNotCreatable: boolean;
  242. // isAbleToDeleteCompletely: boolean,
  243. templateTagData?: string[];
  244. templateBodyData?: string;
  245. isLocalAccountRegistrationEnabled: boolean;
  246. isSearchServiceConfigured: boolean;
  247. isSearchServiceReachable: boolean;
  248. isSearchScopeChildrenAsDefault: boolean;
  249. elasticsearchMaxBodyLengthToIndex: number;
  250. isEnabledMarp: boolean;
  251. isRomUserAllowedToComment: boolean;
  252. sidebarConfig: ISidebarConfig;
  253. isSlackConfigured: boolean;
  254. // isMailerSetup: boolean,
  255. isAclEnabled: boolean;
  256. // hasSlackConfig: boolean,
  257. drawioUri: string | null;
  258. // highlightJsStyle: string,
  259. isAllReplyShown: boolean;
  260. showPageSideAuthors: boolean;
  261. isContainerFluid: boolean;
  262. isUploadEnabled: boolean;
  263. isUploadAllFileAllowed: boolean;
  264. isBulkExportPagesEnabled: boolean;
  265. isPdfBulkExportEnabled: boolean;
  266. isEnabledStaleNotification: boolean;
  267. isEnabledAttachTitleHeader: boolean;
  268. // isEnabledLinebreaks: boolean,
  269. // isEnabledLinebreaksInComments: boolean,
  270. adminPreferredIndentSize: number;
  271. isIndentSizeForced: boolean;
  272. disableLinkSharing: boolean;
  273. skipSSR: boolean;
  274. ssrMaxRevisionBodyLength: number;
  275. yjsData: CurrentPageYjsData;
  276. rendererConfig: RendererConfig;
  277. aiEnabled: boolean;
  278. limitLearnablePageCountPerAssistant: number;
  279. isUsersHomepageDeletionEnabled: boolean;
  280. };
  281. const Page: NextPageWithLayout<Props> = (props: Props) => {
  282. const router = useRouter();
  283. useCurrentUser(props.currentUser ?? null);
  284. // commons
  285. useGrowiCloudUri(props.growiCloudUri);
  286. // page
  287. useIsContainerFluid(props.isContainerFluid);
  288. // useOwnerOfCurrentPage(props.pageUser != null ? JSON.parse(props.pageUser) : null);
  289. useIsForbidden(props.isForbidden);
  290. useIsNotCreatable(props.isNotCreatable);
  291. useRedirectFrom(props.redirectFrom ?? null);
  292. useIsSharedUser(false); // this page cann't be routed for '/share'
  293. useIsIdenticalPath(props.isIdenticalPathPage ?? false);
  294. useIsEnabledStaleNotification(props.isEnabledStaleNotification);
  295. useIsSearchPage(false);
  296. useIsEnabledAttachTitleHeader(props.isEnabledAttachTitleHeader);
  297. useIsSearchServiceConfigured(props.isSearchServiceConfigured);
  298. useIsSearchServiceReachable(props.isSearchServiceReachable);
  299. useElasticsearchMaxBodyLengthToIndex(props.elasticsearchMaxBodyLengthToIndex);
  300. useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
  301. useIsSlackConfigured(props.isSlackConfigured);
  302. // useIsMailerSetup(props.isMailerSetup);
  303. useIsAclEnabled(props.isAclEnabled);
  304. // useHasSlackConfig(props.hasSlackConfig);
  305. useDefaultIndentSize(props.adminPreferredIndentSize);
  306. useIsIndentSizeForced(props.isIndentSizeForced);
  307. useDisableLinkSharing(props.disableLinkSharing);
  308. useRendererConfig(props.rendererConfig);
  309. useIsEnabledMarp(props.rendererConfig.isEnabledMarp);
  310. // useRendererSettings(props.rendererSettingsStr != null ? JSON.parse(props.rendererSettingsStr) : undefined);
  311. // useGrowiRendererConfig(props.growiRendererConfigStr != null ? JSON.parse(props.growiRendererConfigStr) : undefined);
  312. useIsAllReplyShown(props.isAllReplyShown);
  313. useShowPageSideAuthors(props.showPageSideAuthors);
  314. useIsUploadAllFileAllowed(props.isUploadAllFileAllowed);
  315. useIsUploadEnabled(props.isUploadEnabled);
  316. useIsBulkExportPagesEnabled(props.isBulkExportPagesEnabled);
  317. useIsPdfBulkExportEnabled(props.isPdfBulkExportEnabled);
  318. useIsLocalAccountRegistrationEnabled(props.isLocalAccountRegistrationEnabled);
  319. useIsRomUserAllowedToComment(props.isRomUserAllowedToComment);
  320. useIsAiEnabled(props.aiEnabled);
  321. useLimitLearnablePageCountPerAssistant(
  322. props.limitLearnablePageCountPerAssistant,
  323. );
  324. useIsUsersHomepageDeletionEnabled(props.isUsersHomepageDeletionEnabled);
  325. const { pageWithMeta } = props;
  326. const pageId = pageWithMeta?.data._id;
  327. const revisionId = pageWithMeta?.data.revision?._id;
  328. const revisionBody = pageWithMeta?.data.revision?.body;
  329. useCurrentPathname(props.currentPathname);
  330. const { data: currentPage } = useSWRxCurrentPage(pageWithMeta?.data ?? null); // store initial data
  331. const { trigger: mutateCurrentPage } = useSWRMUTxCurrentPage();
  332. const { trigger: mutateCurrentPageYjsDataFromApi } =
  333. useSWRMUTxCurrentPageYjsData();
  334. const { mutate: mutateEditingMarkdown } = useEditingMarkdown();
  335. const { data: currentPageId, mutate: mutateCurrentPageId } =
  336. useCurrentPageId();
  337. const { data: isGuestUser } = useIsGuestUser();
  338. const { mutate: mutateIsNotFound } = useIsNotFound();
  339. const { mutate: mutateIsLatestRevision } = useIsLatestRevision();
  340. const { mutate: mutateRemoteRevisionId } = useRemoteRevisionId();
  341. const { mutate: mutateTemplateTagData } = useTemplateTagData();
  342. const { mutate: mutateTemplateBodyData } = useTemplateBodyData();
  343. const { mutate: mutateCurrentPageYjsData } = useCurrentPageYjsData();
  344. useSetupGlobalSocket();
  345. useSetupGlobalSocketForPage(pageId);
  346. // Store initial data (When revisionBody is not SSR)
  347. useEffect(() => {
  348. if (!props.skipSSR) {
  349. return;
  350. }
  351. if (currentPageId != null && revisionId != null && !props.isNotFound) {
  352. const mutatePageData = async () => {
  353. const pageData = await mutateCurrentPage();
  354. mutateEditingMarkdown(pageData?.revision?.body);
  355. };
  356. // If skipSSR is true, use the API to retrieve page data.
  357. // Because pageWIthMeta does not contain revision.body
  358. mutatePageData();
  359. }
  360. }, [
  361. revisionId,
  362. currentPageId,
  363. mutateCurrentPage,
  364. mutateEditingMarkdown,
  365. props.isNotFound,
  366. props.skipSSR,
  367. ]);
  368. // Load current yjs data
  369. useEffect(() => {
  370. if (
  371. !isGuestUser &&
  372. currentPageId != null &&
  373. revisionId != null &&
  374. mutateCurrentPageYjsDataFromApi != null &&
  375. !props.isNotFound
  376. ) {
  377. mutateCurrentPageYjsDataFromApi();
  378. }
  379. }, [
  380. isGuestUser,
  381. currentPageId,
  382. mutateCurrentPageYjsDataFromApi,
  383. props.isNotFound,
  384. revisionId,
  385. ]);
  386. // sync pathname by Shallow Routing https://nextjs.org/docs/routing/shallow-routing
  387. useEffect(() => {
  388. const decodedURI = decodeURI(window.location.pathname);
  389. if (isClient() && decodedURI !== props.currentPathname) {
  390. const { search, hash } = window.location;
  391. router.replace(`${props.currentPathname}${search}${hash}`, undefined, {
  392. shallow: true,
  393. });
  394. }
  395. }, [props.currentPathname, router]);
  396. // initialize mutateEditingMarkdown only once per page
  397. // need to include useCurrentPathname not useCurrentPagePath
  398. useEffect(() => {
  399. if (props.currentPathname != null) {
  400. mutateEditingMarkdown(revisionBody);
  401. }
  402. }, [mutateEditingMarkdown, revisionBody, props.currentPathname]);
  403. useEffect(() => {
  404. mutateRemoteRevisionId(revisionId);
  405. }, [mutateRemoteRevisionId, revisionId]);
  406. useEffect(() => {
  407. mutateCurrentPageId(pageId ?? null);
  408. }, [mutateCurrentPageId, pageId]);
  409. useEffect(() => {
  410. mutateIsNotFound(props.isNotFound);
  411. }, [mutateIsNotFound, props.isNotFound]);
  412. useEffect(() => {
  413. mutateIsLatestRevision(props.isLatestRevision);
  414. }, [mutateIsLatestRevision, props.isLatestRevision]);
  415. useEffect(() => {
  416. mutateTemplateTagData(props.templateTagData);
  417. }, [props.templateTagData, mutateTemplateTagData]);
  418. useEffect(() => {
  419. mutateTemplateBodyData(props.templateBodyData);
  420. }, [props.templateBodyData, mutateTemplateBodyData]);
  421. useEffect(() => {
  422. mutateCurrentPageYjsData(props.yjsData);
  423. }, [mutateCurrentPageYjsData, props.yjsData]);
  424. // If the data on the page changes without router.push, pageWithMeta remains old because getServerSideProps() is not executed
  425. // So preferentially take page data from useSWRxCurrentPage
  426. const pagePath =
  427. currentPage?.path ?? pageWithMeta?.data.path ?? props.currentPathname;
  428. const title = generateCustomTitleForPage(props, pagePath);
  429. return (
  430. <>
  431. <Head>
  432. <title>{title}</title>
  433. </Head>
  434. <div className="dynamic-layout-root justify-content-between">
  435. <GrowiContextualSubNavigation
  436. isLinkSharingDisabled={props.disableLinkSharing}
  437. />
  438. <PageView
  439. className="d-edit-none"
  440. pagePath={pagePath}
  441. initialPage={pageWithMeta?.data}
  442. rendererConfig={props.rendererConfig}
  443. />
  444. <EditablePageEffects />
  445. <DisplaySwitcher />
  446. <PageStatusAlert />
  447. </div>
  448. </>
  449. );
  450. };
  451. const BasicLayoutWithEditor = ({
  452. children,
  453. }: {
  454. children?: ReactNode;
  455. }): JSX.Element => {
  456. const editorModeClassName = useEditorModeClassName();
  457. return <BasicLayout className={editorModeClassName}>{children}</BasicLayout>;
  458. };
  459. type LayoutProps = Props & {
  460. children?: ReactNode;
  461. };
  462. const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
  463. // init sidebar config with UserUISettings and sidebarConfig
  464. useInitSidebarConfig(props.sidebarConfig, props.userUISettings);
  465. return <BasicLayoutWithEditor>{children}</BasicLayoutWithEditor>;
  466. };
  467. Page.getLayout = function getLayout(page: React.ReactElement<Props>) {
  468. return (
  469. <>
  470. <GrowiPluginsActivator />
  471. <DrawioViewerScript drawioUri={page.props.rendererConfig.drawioUri} />
  472. <Layout {...page.props}>{page}</Layout>
  473. <UnsavedAlertDialog />
  474. <DescendantsPageListModal />
  475. <DrawioModal />
  476. <HandsontableModal />
  477. <TemplateModal />
  478. <LinkEditModal />
  479. <TagEditModal />
  480. <ConflictDiffModal />
  481. </>
  482. );
  483. };
  484. function getPageIdFromPathname(currentPathname: string): string | null {
  485. return _isPermalink(currentPathname)
  486. ? removeHeadingSlash(currentPathname)
  487. : null;
  488. }
  489. class MultiplePagesHitsError extends ExtensibleCustomError {
  490. pagePath: string;
  491. constructor(pagePath: string) {
  492. super(`MultiplePagesHitsError occured by '${pagePath}'`);
  493. this.pagePath = pagePath;
  494. }
  495. }
  496. async function injectPageData(
  497. context: GetServerSidePropsContext,
  498. props: Props,
  499. ): Promise<void> {
  500. const { model: mongooseModel } = await import('mongoose');
  501. const req: CrowiRequest = context.req as CrowiRequest;
  502. const { crowi } = req;
  503. const { revisionId } = req.query;
  504. const Page = crowi.model('Page') as PageModel;
  505. const PageRedirect = mongooseModel('PageRedirect') as PageRedirectModel;
  506. const { pageService, configManager } = crowi;
  507. let currentPathname = props.currentPathname;
  508. const pageId = getPageIdFromPathname(currentPathname);
  509. const isPermalink = _isPermalink(currentPathname);
  510. const { user } = req;
  511. if (!isPermalink) {
  512. // check redirects
  513. const chains =
  514. await PageRedirect.retrievePageRedirectEndpoints(currentPathname);
  515. if (chains != null) {
  516. // overwrite currentPathname
  517. currentPathname = chains.end.toPath;
  518. props.currentPathname = currentPathname;
  519. // set redirectFrom
  520. props.redirectFrom = chains.start.fromPath;
  521. }
  522. // check whether the specified page path hits to multiple pages
  523. const count = await Page.countByPathAndViewer(
  524. currentPathname,
  525. user,
  526. null,
  527. true,
  528. );
  529. if (count > 1) {
  530. throw new MultiplePagesHitsError(currentPathname);
  531. }
  532. }
  533. const pageWithMeta = await pageService.findPageAndMetaDataByViewer(
  534. pageId,
  535. currentPathname,
  536. user,
  537. true,
  538. ); // includeEmpty = true, isSharedPage = false
  539. const { data: page, meta } = pageWithMeta ?? {};
  540. // add user to seen users
  541. if (page != null && user != null) {
  542. await page.seen(user);
  543. }
  544. props.pageWithMeta = null;
  545. // populate & check if the revision is latest
  546. if (page != null) {
  547. page.initLatestRevisionField(revisionId);
  548. props.isLatestRevision = page.isLatestRevision();
  549. const ssrMaxRevisionBodyLength = configManager.getConfig(
  550. 'app:ssrMaxRevisionBodyLength',
  551. );
  552. props.skipSSR = await skipSSR(page, ssrMaxRevisionBodyLength);
  553. const populatedPage = await page.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
  554. props.pageWithMeta = {
  555. data: populatedPage,
  556. meta,
  557. };
  558. }
  559. }
  560. async function injectRoutingInformation(
  561. context: GetServerSidePropsContext,
  562. props: Props,
  563. ): Promise<void> {
  564. const req: CrowiRequest = context.req as CrowiRequest;
  565. const { crowi } = req;
  566. const Page = crowi.model('Page') as PageModel;
  567. const { currentPathname } = props;
  568. const pageId = getPageIdFromPathname(currentPathname);
  569. const isPermalink = _isPermalink(currentPathname);
  570. const page = props.pageWithMeta?.data;
  571. if (props.isIdenticalPathPage) {
  572. props.isNotCreatable = true;
  573. } else if (page == null) {
  574. props.isNotFound = true;
  575. props.isNotCreatable = !isCreatablePage(currentPathname);
  576. // check the page is forbidden or just does not exist.
  577. const count = isPermalink
  578. ? await Page.count({ _id: pageId })
  579. : await Page.count({ path: currentPathname });
  580. props.isForbidden = count > 0;
  581. } else {
  582. props.isNotFound = page.isEmpty;
  583. props.isNotCreatable = false;
  584. props.isForbidden = false;
  585. // /62a88db47fed8b2d94f30000 ==> /path/to/page
  586. if (isPermalink && page.isEmpty) {
  587. props.currentPathname = page.path;
  588. }
  589. // /path/to/page ==> /62a88db47fed8b2d94f30000
  590. if (!isPermalink && !page.isEmpty) {
  591. const isToppage = pagePathUtils.isTopPage(props.currentPathname);
  592. if (!isToppage) {
  593. props.currentPathname = `/${page._id}`;
  594. }
  595. }
  596. if (!props.skipSSR) {
  597. props.yjsData = await crowi.pageService.getYjsData(page._id.toString());
  598. }
  599. }
  600. }
  601. // async function injectPageUserInformation(context: GetServerSidePropsContext, props: Props): Promise<void> {
  602. // const req: CrowiRequest = context.req as CrowiRequest;
  603. // const { crowi } = req;
  604. // const UserModel = crowi.model('User');
  605. // if (isUserPage(props.currentPagePath)) {
  606. // const user = await UserModel.findUserByUsername(UserModel.getUsernameByPath(props.currentPagePath));
  607. // if (user != null) {
  608. // props.pageUser = JSON.stringify(user.toObject());
  609. // }
  610. // }
  611. // }
  612. function injectServerConfigurations(
  613. context: GetServerSidePropsContext,
  614. props: Props,
  615. ): void {
  616. const req: CrowiRequest = context.req as CrowiRequest;
  617. const { crowi } = req;
  618. const {
  619. configManager,
  620. searchService,
  621. aclService,
  622. fileUploadService,
  623. slackIntegrationService,
  624. passportService,
  625. } = crowi;
  626. props.aiEnabled = configManager.getConfig('app:aiEnabled');
  627. props.limitLearnablePageCountPerAssistant = configManager.getConfig(
  628. 'openai:limitLearnablePageCountPerAssistant',
  629. );
  630. props.isUsersHomepageDeletionEnabled = configManager.getConfig(
  631. 'security:user-homepage-deletion:isEnabled',
  632. );
  633. props.isSearchServiceConfigured = searchService.isConfigured;
  634. props.isSearchServiceReachable = searchService.isReachable;
  635. props.isSearchScopeChildrenAsDefault = configManager.getConfig(
  636. 'customize:isSearchScopeChildrenAsDefault',
  637. );
  638. props.elasticsearchMaxBodyLengthToIndex = configManager.getConfig(
  639. 'app:elasticsearchMaxBodyLengthToIndex',
  640. );
  641. props.isRomUserAllowedToComment = configManager.getConfig(
  642. 'security:isRomUserAllowedToComment',
  643. );
  644. props.isSlackConfigured = slackIntegrationService.isSlackConfigured;
  645. // props.isMailerSetup = mailService.isMailerSetup;
  646. props.isAclEnabled = aclService.isAclEnabled();
  647. // props.hasSlackConfig = slackNotificationService.hasSlackConfig();
  648. props.drawioUri = configManager.getConfig('app:drawioUri');
  649. // props.highlightJsStyle = configManager.getConfig('customize:highlightJsStyle');
  650. props.isAllReplyShown = configManager.getConfig('customize:isAllReplyShown');
  651. props.showPageSideAuthors = configManager.getConfig(
  652. 'customize:showPageSideAuthors',
  653. );
  654. props.isContainerFluid = configManager.getConfig(
  655. 'customize:isContainerFluid',
  656. );
  657. props.isEnabledStaleNotification = configManager.getConfig(
  658. 'customize:isEnabledStaleNotification',
  659. );
  660. props.disableLinkSharing = configManager.getConfig(
  661. 'security:disableLinkSharing',
  662. );
  663. props.isUploadAllFileAllowed = fileUploadService.getFileUploadEnabled();
  664. props.isUploadEnabled = fileUploadService.getIsUploadable();
  665. // TODO: remove growiCloudUri condition when bulk export can be relased for GROWI.cloud (https://redmine.weseek.co.jp/issues/163220)
  666. props.isBulkExportPagesEnabled =
  667. configManager.getConfig('app:isBulkExportPagesEnabled') &&
  668. configManager.getConfig('app:growiCloudUri') == null;
  669. props.isPdfBulkExportEnabled =
  670. configManager.getConfig('app:pageBulkExportPdfConverterUri') != null;
  671. props.isLocalAccountRegistrationEnabled =
  672. passportService.isLocalStrategySetup &&
  673. configManager.getConfig('security:registrationMode') !==
  674. RegistrationMode.CLOSED;
  675. props.adminPreferredIndentSize = configManager.getConfig(
  676. 'markdown:adminPreferredIndentSize',
  677. );
  678. props.isIndentSizeForced = configManager.getConfig(
  679. 'markdown:isIndentSizeForced',
  680. );
  681. props.isEnabledAttachTitleHeader = configManager.getConfig(
  682. 'customize:isEnabledAttachTitleHeader',
  683. );
  684. props.sidebarConfig = {
  685. isSidebarCollapsedMode: configManager.getConfig(
  686. 'customize:isSidebarCollapsedMode',
  687. ),
  688. isSidebarClosedAtDockMode: configManager.getConfig(
  689. 'customize:isSidebarClosedAtDockMode',
  690. ),
  691. };
  692. props.rendererConfig = {
  693. isEnabledLinebreaks: configManager.getConfig(
  694. 'markdown:isEnabledLinebreaks',
  695. ),
  696. isEnabledLinebreaksInComments: configManager.getConfig(
  697. 'markdown:isEnabledLinebreaksInComments',
  698. ),
  699. isEnabledMarp: configManager.getConfig('customize:isEnabledMarp'),
  700. adminPreferredIndentSize: configManager.getConfig(
  701. 'markdown:adminPreferredIndentSize',
  702. ),
  703. isIndentSizeForced: configManager.getConfig('markdown:isIndentSizeForced'),
  704. drawioUri: configManager.getConfig('app:drawioUri'),
  705. plantumlUri: configManager.getConfig('app:plantumlUri'),
  706. // XSS Options
  707. isEnabledXssPrevention: configManager.getConfig(
  708. 'markdown:rehypeSanitize:isEnabledPrevention',
  709. ),
  710. sanitizeType: configManager.getConfig('markdown:rehypeSanitize:option'),
  711. customTagWhitelist: configManager.getConfig(
  712. 'markdown:rehypeSanitize:tagNames',
  713. ),
  714. customAttrWhitelist:
  715. configManager.getConfig('markdown:rehypeSanitize:attributes') != null
  716. ? JSON.parse(
  717. configManager.getConfig('markdown:rehypeSanitize:attributes'),
  718. )
  719. : undefined,
  720. highlightJsStyleBorder: configManager.getConfig(
  721. 'customize:highlightJsStyleBorder',
  722. ),
  723. };
  724. props.ssrMaxRevisionBodyLength = configManager.getConfig(
  725. 'app:ssrMaxRevisionBodyLength',
  726. );
  727. }
  728. /**
  729. * for Server Side Translations
  730. * @param context
  731. * @param props
  732. * @param namespacesRequired
  733. */
  734. async function injectNextI18NextConfigurations(
  735. context: GetServerSidePropsContext,
  736. props: Props,
  737. namespacesRequired?: string[] | undefined,
  738. ): Promise<void> {
  739. const nextI18NextConfig = await getNextI18NextConfig(
  740. serverSideTranslations,
  741. context,
  742. namespacesRequired,
  743. );
  744. props._nextI18Next = nextI18NextConfig._nextI18Next;
  745. }
  746. const getAction = (props: Props): SupportedActionType => {
  747. if (props.isNotCreatable) {
  748. return SupportedAction.ACTION_PAGE_NOT_CREATABLE;
  749. }
  750. if (props.isForbidden) {
  751. return SupportedAction.ACTION_PAGE_FORBIDDEN;
  752. }
  753. if (props.isNotFound) {
  754. return SupportedAction.ACTION_PAGE_NOT_FOUND;
  755. }
  756. if (pagePathUtils.isUsersHomepage(props.pageWithMeta?.data.path ?? '')) {
  757. return SupportedAction.ACTION_PAGE_USER_HOME_VIEW;
  758. }
  759. return SupportedAction.ACTION_PAGE_VIEW;
  760. };
  761. export const getServerSideProps: GetServerSideProps = async (
  762. context: GetServerSidePropsContext,
  763. ) => {
  764. const req = context.req as CrowiRequest;
  765. const { user } = req;
  766. const result = await getServerSideCommonProps(context);
  767. // check for presence
  768. // see: https://github.com/vercel/next.js/issues/19271#issuecomment-730006862
  769. if (!('props' in result)) {
  770. throw new Error('invalid getSSP result');
  771. }
  772. const props: Props = result.props as Props;
  773. if (props.redirectDestination != null) {
  774. return {
  775. redirect: {
  776. permanent: false,
  777. destination: props.redirectDestination,
  778. },
  779. };
  780. }
  781. if (user != null) {
  782. props.currentUser = user.toObject();
  783. }
  784. try {
  785. await injectPageData(context, props);
  786. } catch (err) {
  787. if (err instanceof MultiplePagesHitsError) {
  788. props.isIdenticalPathPage = true;
  789. } else {
  790. throw err;
  791. }
  792. }
  793. await injectRoutingInformation(context, props);
  794. injectServerConfigurations(context, props);
  795. await injectNextI18NextConfigurations(context, props, ['translation']);
  796. addActivity(context, getAction(props));
  797. return {
  798. props,
  799. };
  800. };
  801. export default Page;